package org.summerframework.component.security.spring.access;

import org.summerframework.component.security.annotion.RoleRequired;
import org.summerframework.component.security.core.interceptor.SecurityInterceptor;
import org.summerframework.component.security.spring.annotation.ScopeRequired;
import org.summerframework.core.util.SpringContextUtil;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.method.AbstractMethodSecurityMetadataSource;
import org.springframework.security.access.prepost.PreInvocationAttribute;
import org.springframework.security.access.prepost.PrePostInvocationAttributeFactory;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.util.ClassUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author 石超
 * @version v1.0.0
 */
public class RoleAnnotationSecurityMetadataSource extends AbstractMethodSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private final PrePostInvocationAttributeFactory attributeFactory;
    private RoleAnnotationSecurityMetadataSourceHandler roleAnnotationSecurityMetadataSourceHandler;

    public RoleAnnotationSecurityMetadataSource(PrePostInvocationAttributeFactory attributeFactory) {
        this.attributeFactory = attributeFactory;
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {
        RoleRequired annotation = findAnnotation(method, targetClass, RoleRequired.class);
        ScopeRequired scopeRequired = findAnnotation(method, targetClass, ScopeRequired.class);

        List<ConfigAttribute> attributes = new ArrayList<>();

        if (annotation != null) {
            PreInvocationAttribute preInvocationAttribute;

            if (Arrays.stream(annotation.code()).anyMatch(s -> StringUtils.equals(SecurityInterceptor.ANY, s))) {
                preInvocationAttribute = attributeFactory.createPreInvocationAttribute(null, null, "permitAll()");
            } else {
                List<String> admin = Arrays.stream(annotation.code()).filter(code -> {
                    if (code.startsWith("ROLE_")) {
                        String roleName = code.substring(0, code.length() - 5);
                        return getRoleAnnotationSecurityMetadataSourceHandler().hasRole(roleName);
                    } else {
                        return getRoleAnnotationSecurityMetadataSourceHandler().hasPermission(code);
                    }
                }).map(code -> {
                    if (code.equals(SecurityInterceptor.ADMIN)) {
                        return "'ROLE_" + SecurityInterceptor.ADMIN + "'";
                    } else {
                        return "'" + code + "'";
                    }
                }).collect(Collectors.toList());

                if (CollectionUtils.isEmpty(admin)) {
                    admin.add("'ROLE_" + SecurityInterceptor.ADMIN + "'");
                }

                preInvocationAttribute = attributeFactory.createPreInvocationAttribute(null, null, annotation.isAllRole() ? admin.stream().map(s -> String.format("hasAuthority(%s)", s)).collect(Collectors.joining(" and ")) : String.format("hasAnyAuthority(%s)", StringUtils.join(admin, ",")));
            }

            attributes.add(preInvocationAttribute);
        }

        if (scopeRequired != null) {
            PreInvocationAttribute preInvocationAttribute;

            List<String> admin = Arrays.stream(scopeRequired.value()).map(code -> "'SCOPE_" + code + "'").collect(Collectors.toList());

            preInvocationAttribute = attributeFactory.createPreInvocationAttribute(null, null, annotation != null && annotation.isAllRole() ? admin.stream().map(s -> String.format("hasAuthority(%s)", s)).collect(Collectors.joining(" and ")) : String.format("hasAnyAuthority(%s)", StringUtils.join(admin, ",")));

            attributes.add(preInvocationAttribute);
        }

        return attributes;
    }

    public RoleAnnotationSecurityMetadataSourceHandler getRoleAnnotationSecurityMetadataSourceHandler() {
        if (roleAnnotationSecurityMetadataSourceHandler == null) {
            roleAnnotationSecurityMetadataSourceHandler = SpringContextUtil.getBean(RoleAnnotationSecurityMetadataSourceHandler.class);
        }

        return roleAnnotationSecurityMetadataSourceHandler;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    /**
     * See
     * {@link org.springframework.security.access.method.AbstractFallbackMethodSecurityMetadataSource#getAttributes(Method, Class)}
     * for the logic of this method. The ordering here is slightly different in that we
     * consider method-specific annotations on an interface before class-level ones.
     */
    private <A extends Annotation> A findAnnotation(Method method, Class<?> targetClass,
                                                    Class<A> annotationClass) {
        // The method may be on an interface, but we need attributes from the target
        // class.
        // If the target class is null, the method will be unchanged.
        Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
        A annotation = AnnotatedElementUtils.findMergedAnnotation(specificMethod, annotationClass);

        if (annotation != null) {
            logger.debug(annotation + " found on specific method: " + specificMethod);
            return annotation;
        }

        // Check the original (e.g. interface) method
        if (specificMethod != method) {
            annotation = AnnotatedElementUtils.findMergedAnnotation(method, annotationClass);

            if (annotation != null) {
                logger.debug(annotation + " found on: " + method);
                return annotation;
            }
        }

        // Check the class-level (note declaringClass, not targetClass, which may not
        // actually implement the method)
        annotation = AnnotatedElementUtils.findMergedAnnotation(specificMethod.getDeclaringClass(),
                annotationClass);

        if (annotation != null) {
            logger.debug(annotation + " found on: "
                    + specificMethod.getDeclaringClass().getName());
            return annotation;
        }

        return null;
    }
}
